class Vue {
  constructor(options) {
    this.$options = options;
    this.initData();
    new Observer(this.$options.data);
    this.initWatch();
  }
  initData() {
    const data = (this._data = this.$options.data);
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        get() {
          return this._data[keys[i]];
        },
        set(value) {
          this._data[keys[i]] = value;
        },
      });
    }
  }
  initWatch() {
    const watchs = this.$options.watch;
    const keys = Object.keys(watchs);
    for (let i = 0; i < keys.length; i++) {
      new Watcher(this, keys[i], watchs[keys[i]]);
    }
  }
  $watch(exp, cb) {
    new Watcher(this, exp, cb);
  }
  $set(data, key, value) {
    defineReactive(data,key,value);
    data.__ob__.dep.notify();
  }
}

function observer(data) {
  const type = Object.prototype.toString.call(data);
  if (type !== "[object Object]" && type !== "[object Array]") return;
  // 如果有说明不需要递归定义为响应式了，之前已经被定义过了
  if (data.__ob__) {
    return data.__ob__;
  }
 return new Observer(data);
}

function defineReactive(data, key, value) {
  const dep = new Dep();
  const childObj = observer(value);
  Object.defineProperty(data, key, {
    configurable: true,
    enumerable: true,
    get: function reactiveGetter() {
      if (childObj) {
        childObj.dep.addSub()
      }
      dep.addSub();
      return value;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === data[key]) return;
      dep.notify();
      value = newVal;
    },
  });
}

class Observer {
  constructor(data) {
    this.dep = new Dep();
    this.walk(data);
    Object.defineProperty(data, "__ob__", {
      configurable: true,
      enumerable: false,
      writable: true,
      value: this,
    });
  }
  walk(data) {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }
}

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub() {
    if (Dep.target) {
      this.subs.push(Dep.target);
    }
  }
  notify() {
    this.subs.forEach((watcher) => {
      watcher.update();
    });
  }
}
const watcherIds = [];
let id = 0;
class Watcher {
  constructor(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.id = ++id;
    this.value = this.get();
  }
  get() {
    Dep.target = this;
    const value = parseExpression.call(this.vm, this.exp);
    Dep.target = null;
    return value;
  }
  update() {
    if (watcherIds.indexOf(this.id) !== -1) return;
    watcherIds.push(this.id);
    Promise.resolve().then(() => {
      this.cb.call(this.vm, this.get(), this.value);
      let index = watcherIds.length - 1;
      watcherIds.splice(index, 1);
    });
  }
}

const bailRE = /[^\w.$]/;
function parseExpression(path) {
  if (bailRE.test(path)) return;
  const keys = path.split(".");
  let result = this;
  for (let i = 0; i < keys.length; i++) {
    result = result[keys[i]];
    if (result === undefined) return;
  }
  return result;
}
